package nachos.threads;

import nachos.machine.*;
import java.util.Random;

/**
 * A Tester for the Condition class
 */
public class ConditionTest {

  /**
   * ProdConsBuffer class, which implements a producer/consumer buffer. This implementation uses
   * several condition variables, and some "tricks" to synchronize everything at the end of the
   * execution. This could be done better with a join() call to wait for threads to finish. But at
   * the moment, the main thread waits to be signaled that all producers and consumers have exited.
   * This demonstrate that synchronization primitives can be built on top of others, or each other.
   */
  private static class ProdConsBuffer {

    /**
     * Constructor: takes as parameter the maximum number of actions (i.e., producing or consuming
     * an item) before all producers and consumers call it quit. This is to avoid an infinite
     * execution.
     */
    public ProdConsBuffer(int maxNumActions) {

      /* Initially no actions have been performed */
      this.numActions = 0;
      this.maxNumActions = maxNumActions;
      this.isDone = false;

      /* Initially no threads are done */
      this.numFinishedThreads = 0;

      /* Initially the buffer is empty */
      this.lastItemIndex = -1;
      this.buffer = new int[ProdConsBuffer.maxNumItems];
      for (int i = 0; i < ProdConsBuffer.maxNumItems; i++)
        this.buffer[i] = -1;
      this.isEmpty = true;
      this.isFull = false;

      /* Create the mutex and the two condition variables */
      this.mutex = new Lock();
      this.isNotEmptyCond = new Condition(this.mutex);
      this.isNotFullCond = new Condition(this.mutex);
      this.isOverCond = new Condition(this.mutex);

      /* Create the RNG to generate random items */
      this.rng = new Random();
    }

    /**
     * Method to consume an item. This method is NOT thread-safe and should by called within a
     * critical section via this.mutex. This function should never be called on an empty buffer.
     */
    public int consumeItem() {
      int item;

      /* Is the buffer empty??? */
      if (lastItemIndex == -1) {
        System.out.println("Error: Can't consume item because buffer is empty!!");
        return -1;
      }

      /* Consume the item */
      lastItemIndex--;
      item = buffer[lastItemIndex + 1];
      buffer[lastItemIndex + 1] = -1;

      /* Sanity check */
      if (item == -1) {
        System.out.println("Error: Consumed an invalid item!!");
        return -1;
      }

      /* Update isEmpty and isFull */
      isEmpty = (lastItemIndex == -1);
      isFull = false;

      /* One additional action was performed */
      isDone = (++numActions >= maxNumActions);

      /* if we're done, wake up all sleepers */
      if (isDone) {
        this.isNotFullCond.wakeAll();
        this.isNotEmptyCond.wakeAll();
      }

      return item;
    }

    /**
     * Method to produce an item. This method is NOT thread-safe and should by called within a
     * critical section via this.mutex. This function should never be called on a full buffer.
     */
    public void produceItem(int item) {

      /* Is the buffer full?? */
      if (lastItemIndex == maxNumItems - 1) {
        System.out.println("Error: Can't produce item because buffer is full!!");
        return;
      }

      /* Sanity check */
      if (buffer[lastItemIndex + 1] != -1) {
        System.out.println("Error: Produced item at an invalid position!!");
        return;
      }

      /* Produce an item */
      lastItemIndex++;
      buffer[lastItemIndex] = item;

      /* Update isFull and isEmpty */
      isFull = (lastItemIndex == maxNumItems - 1);
      isEmpty = false;

      /* One additional action was performed */
      isDone = (++numActions >= maxNumActions);

      /* if we're done, wake up all sleepers */
      if (isDone) {
        this.isNotFullCond.wakeAll();
        this.isNotEmptyCond.wakeAll();
      }

      return;
    }

    /**
     * Method to generate a random number
     */
    public int generateRandomItem() {
      return rng.nextInt(50); /* between 0 and 50 */
    }

    /* Lock for mutual exclusion and condition variables */
    public Lock mutex;

    /* Condition variables */
    public Condition isNotEmptyCond; /* signaled when the buffer becomes non-empty */
    public Condition isNotFullCond; /* signaled when the buffer becomes non-full */
    public Condition isOverCond; /* signaled by each finishing prod or cons */

    /* Booleans indicating buffer state */
    public boolean isEmpty;
    public boolean isFull;

    /* The buffer of elements */
    private static final int maxNumItems = 10;
    private int lastItemIndex;
    private int buffer[];

    /* The global counter of actions and flag */
    private int maxNumActions;
    private int numActions;
    public boolean isDone;

    /* The number of threads that are finished */
    public int numFinishedThreads;

    /* Random number generator */
    private Random rng;
  }

  /**
   * Producer class, which implements a producer thread that puts data in a buffer.
   */
  private static class Producer implements Runnable {

    /* Constructor */
    Producer(int who, ProdConsBuffer buffer) {
      this.buffer = buffer;
      this.who = who;
    }

    public void run() {

      System.out.println("** Producer #" + who + " begins");
      /* Loop */
      while (true) {
        /* Acquire the mutex */
        buffer.mutex.acquire();
        /*
         * If the buffer is full, wait on isNotFullCond. This is in a while loop to avoid spurious
         * wake-ups
         */
        while (!buffer.isDone && buffer.isFull) {
          System.out.println("** Producer #" + who + " waits for the buffer to not be full");
          buffer.isNotFullCond.sleep(); /*
                                         * releases the mutex and reacquires it when it wakes up
                                         */
        }

        /*
         * I just woke up, and perhaps it's because it's all over in which case I exit from my main
         * loop
         */
        if (buffer.isDone) {
          buffer.mutex.release();
          break;
        }
        /* Produce an item */
        int producedItem = buffer.generateRandomItem();
        System.out.println("** Producer #" + who + " produces " + producedItem);
        buffer.produceItem(producedItem);
        /* Wake up potential consumers */
        buffer.isNotEmptyCond.wake();
        /* Release the mutex */
        buffer.mutex.release();
        /* Yield so that somebody else has a chance to run */
        KThread.yield();
      }
      System.out.println("** Producer #" + who + " exits");

      /* Signal that the thread is finished */
      buffer.mutex.acquire();
      buffer.numFinishedThreads++;
      buffer.isOverCond.wake();
      buffer.mutex.release();
    }

    /* The Prod/Cons buffer */
    private ProdConsBuffer buffer;
    /* An ID for printing out information */
    private int who;
  }

  private static class Consumer implements Runnable {
    Consumer(int who, ProdConsBuffer buffer) {
      this.buffer = buffer;
      this.who = who;
    }

    public void run() {

      System.out.println("** Consumer #" + who + " begins");
      /* Loop */
      while (true) {

        /* Acquire the mutex */
        buffer.mutex.acquire();
        /*
         * If the buffer is empty, wait on isNotEmptyCond. This is in a while loop to avoid spurious
         * wake-ups
         */
        while (!buffer.isDone && buffer.isEmpty) {
          System.out.println("** Consumer #" + who + " waits for the buffer to not be empty");
          buffer.isNotEmptyCond.sleep(); /*
                                          * releases the mutex and reacquires it when it wakes up
                                          */
        }

        /*
         * I just woke up, and perhaps it's because it's all over in which case I exit from my main
         * loop
         */
        if (buffer.isDone) {
          buffer.mutex.release();
          break;
        }

        /* Consume an item */
        int consumedItem = buffer.consumeItem();
        System.out.println("** Consumer #" + who + " consumes item " + consumedItem);
        /* Wake up potential producers */
        buffer.isNotFullCond.wake();
        /* Release the mutex */
        buffer.mutex.release();
        /* Yield so that somebody else has a chance to run */
        KThread.yield();
      }
      System.out.println("** Consumer #" + who + " exits");

      /* Signal that the thread is finished */
      buffer.mutex.acquire();
      buffer.numFinishedThreads++;
      buffer.isOverCond.wake();
      buffer.mutex.release();
    }

    /* The Prod/Cons buffer */
    private ProdConsBuffer buffer;
    /* An ID for printing out information */
    private int who;
  }

  /**
   * Tests whether this module is working.
   */
  public static void runTest() {

    System.out.println("**** Condition testing begins ****");

    /* Create the buffer, with a specified max # of actions */
    ProdConsBuffer buffer = new ProdConsBuffer(maxNumActions);

    /* Create producer threads and fork them */
    KThread producers[] = new KThread[numProducers];
    for (int i = 0; i < numProducers; i++) {
      producers[i] = new KThread(new Producer(i, buffer)).setName("producer thread #" + i);
      producers[i].fork();
    }

    /* Create consumer threads and fork them */
    KThread consumers[] = new KThread[numConsumers];
    for (int i = 0; i < numConsumers; i++) {
      consumers[i] = new KThread(new Consumer(i, buffer)).setName("consumer thread #" + i);
      consumers[i].fork();
    }

    /* Wait for the prod/cons execution to be over */
    buffer.mutex.acquire();
    while (buffer.numFinishedThreads != numConsumers + numProducers) {
      buffer.isOverCond.sleep();
    }
    buffer.mutex.release();

    System.out.println("**** Condition testing ends ****");

  }

  private static final int maxNumActions = 100;
  private static final int numProducers = 4;
  private static final int numConsumers = 10;

}
